package org.groovyflow.db;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.groovyflow.util.IgnoreCaseMap;
import org.groovyflow.GroovyFlowContext;

/**
 * A work in progress.
 * @author Chuck Tassoni
 */
//TODO ??Might store null values as NULL so that we can throw an exception when
//someone references a value that's not valid.  If we do so, then we'll need
//to be smart enough to convert NULL to null when someone retrieves it and
//when we write to the database.  Would also need to convert null to NULL 
//when going the opposite direction.
//Also see note/ActiveRecordTodo.txt
public class ActiveRecord implements java.io.Serializable{
	//Table values
	private Map values = new IgnoreCaseMap()
	//Non-table values that occurred in the sql that populated this object
	private Map extValues = new IgnoreCaseMap()	
	private boolean existsInDb = false
	
	public ActiveRecord(){
		println 'No arg constructor of ActiveRecord called'
		gMetaData()
	}
	
	public ActiveRecord(Map map){
		println 'Map constructor of ActiveRecord called'
		this.sValues(map)
	}
	
	//Why method names such as gMetaData, sValues? Well, for now I'm
	//trying not to use getMetaData, setValues.  That's because the current
	//implementation depends on getProperty and setProperty to delegate most
	//get and sets to maps holding record values.  Soon I'll probably
	//add a Set of property names, and getProperty will add a line that looks like:
	//if(propNames.contains(key)) return  this.@"${key}"
	//The alternative would be the more elegant, but less performant method
	//missing.  By the way, I still haven't figured out how to have ActiveRecords
	//respond correctly to rec.properties and rec.metaClass.  See 
	//http://www.nabble.com/Question-about-overriding-getProperty-setProperty-td18433982.html
	def gMetaData(){
		println 'attempting to find metaData'
		Map meta = gSqlDao().getTableCache().get(gTableName())
		println "metaData at first is " + meta
		if(meta == null){
			println 'metaData was not there, so now calling findTableMetaData'
			DbHelp.inTrx( {gSqlDao().findTableMetaData(this)} );
		}
		meta = gSqlDao().getTableCache().get(gTableName())
		println 'metaData that gMetaData is returning is ' + meta
		return meta;
	}
	
	public SqlDao gSqlDao(){
		return (SqlDao) GroovyFlowContext.get("sqlDao");
	}	
	
	public String gIdName(){
		return "id";
	}
	
	public String gTableName(){
		return this.getClass().getSimpleName();
	}

	public String gPKSequenceName(){
		return this.gTableName() + "_seq";
	}
	
	public String gVersionFieldName(){
		return null;
	}
	
	public Set gFieldsToExclude(){
		return new HashSet();
	}
	
	boolean gExistsInDb(){
		return this.existsInDb
	}
	
	void sExistsInDb(tf){
		this.existsInDb = tf
	}

	def getProperty(String str){   
      	def ret = values[str]
      	return (ret != null) ? ret : extValues[str]
	}
	
	 void setProperty(String str, value){  
	 	//We don't support setting external values, since they can't
	 	//be modified in the database.  Should we throw an
	 	//exception if someone attempts such a set?
      	values[str] = value
	}	
	
	def sValues(Map map){
		println 'sVaues has been called'
		println 'before getting tableColumnNames'
	    def tableColumnNamesButNotId = gMetaData().get(SqlDao.COLUMN_NAMES)
	    //The names here also don't include any fields we declared to be excluded.
	    println'after getting tableColumnNames '
	    
	    def idName = gIdName()
	    //Would be nice to just add the idName to the columns collection, but by doing so
	    //we'd add it to the values in the cache as well.  And we don't want to require 
	    //the cache to clone the columnNames collection on each get.
	    
	    //this.values doesn't work inside the closure, so I create v and e here.
	    //Strangely, the extra variables aren't needed in the GroovyConsole.
	    def v = this.values
	    def e = this.extValues
	    map.each{key, value ->
	    	if(tableColumnNamesButNotId.contains(key) || idName == key)
	    		v[key] = value
	    	else
	    		e[key] = value	
	    }
		this
	}
	
	Map gValues(){
		values
	}
	
	def save(){
		DbHelp.inTrx({
			if(gExistsInDb())
				gSqlDao().doActiveRecordUpdate(this.gValues(), this)
			else{
				gSqlDao().doActiveRecordInsert(this.gValues(), this)
				sExistsInDb(true)
			}
		})
	}
	
	def findById(id){
		 findByWhere(gIdName() + " = ? ", [id])[0]
	}
	
	//TODO  Should we discourage this simply by not having it?
	def findAll(){
		findBySql("select * from " + gTableName(), [])
	}
	
	def findByWhere(String whereAndOrder, List params){
	    findBySql("select * from " + gTableName() + " where " + whereAndOrder, params)
	}	
	
	/**
	* Be sure to select all the fields that you didn't mark as excluded.
	* You can add fields from other tables too-- of course, only the
	* fields belonging to the Record's table will actually be saved.
	*/
	def findBySql(String theSql, List params){
		def clos = { 
			def listOfMaps = gSqlDao().doSearch(theSql, params)
			println "listOfMaps " + listOfMaps 
			def records = []
			
			def className = this.getClass().getName()
			//Inside conditionally-compiled code simple Class.forName doesn't work
			//In fact, now that we're in a closure, getClass().getClassLoader() doesn't work.
			//We need to do delegate.getClass().getClassLoader()
            //That's because, while delegate and this show the same classname,
            //they're not the same object, and I suppose they have diffent classLoaders.
			def clazz = Class.forName(className, true, delegate.getClass().getClassLoader())
			listOfMaps.each{ m ->
			    def one = clazz.newInstance()
				one.sValues(m)
				one.sExistsInDb(true)
				records << one
			}
			return records	
		}
		return DbHelp.inTrx(clos)
	}


	
}
